/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ts, native, HomeCheckHint } from 'qvog-dsl';

const SEVERITY = 2;
const DOC_PATH = 'docs/adjacent-overload-signatures.md';
const DESCRIPTION = 'All overload signatures should be adjacent';
type Member = ts.Node;
export default native<HomeCheckHint>()()
  .match({
    is: (node: ts.Node): node is ts.Node =>
      ts.isClassDeclaration(node) ||
      ts.isInterfaceDeclaration(node) ||
      ts.isModuleDeclaration(node) ||
      ts.isTypeLiteralNode(node)
  })
  .when((node: ts.Node) => {
    const members = getNodeMembers(node);
    if (!members) { return false };

    let lastMethod: Method | null = null;
    const seenMethods: Method[] = [];

    for (const member of members) {
      const method = getMemberMethod(member, node.getSourceFile());
      if (!method) {
        lastMethod = null;
        continue;
      }

      const existingIndex = seenMethods.findIndex(m =>
        isSameMethod(method, m)
      );

      if (existingIndex >= 0 && !isSameMethod(method, lastMethod!)) {
        return true;
      }

      if (existingIndex === -1) {
        seenMethods.push(method);
      }
      lastMethod = method;
    }

    return false;
  })
  .report({
    severity: SEVERITY,
    description: DESCRIPTION,
    docPath: DOC_PATH
  });

type Method = {
  name: string;
  static: boolean;
  line: number;
  character: number;
  endCol: number;
};

function getMemberMethod(member: ts.Node, sourceFile: ts.SourceFile): Method | null {
  const position = member.getStart();
  const endPosition = member.getEnd();
  const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, position);
  const { character: endChar } = ts.getLineAndCharacterOfPosition(sourceFile, endPosition);

  if (ts.isCallSignatureDeclaration(member)) {
    return { name: ' call ', static: false, line: line + 1, character: character + 1, endCol: endChar + 1 };
  }
  if (ts.isConstructorDeclaration(member)) {
    return { name: ' construct ', static: false, line: line + 1, character: character + 1, endCol: endChar + 1 };
  }

  let name: string | null = null;
  let isStatic = false;

  if (ts.isMethodSignature(member) || ts.isMethodDeclaration(member)) {
    if (member.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword)) {
      return null;
    }
    if (member.name && ts.isComputedPropertyName(member.name)) {
      const expr = member.name.expression;
      name = ts.isStringLiteral(expr) ? expr.text :
        ts.isIdentifier(expr) ? expr.text : null;
    } else if (ts.isIdentifier(member.name)) {
      name = member.name.text;
    } else if (ts.isPrivateIdentifier(member.name)) {
      name = `#${member.name.text}`;
    }
    isStatic = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword) ?? false;
  } else if (ts.isFunctionDeclaration(member)) {
    name = member.name?.text ?? null;
  }

  return name ? { name, static: isStatic, line: line + 1, character: character + 1, endCol: endChar + 1 } : null;
}

function isSameMethod(method1: Method, method2: Method): boolean {
  if (method1 === null || method2 === null) { return false };
  if (method1.name === ' call ' && method2.name === ' call ') { return true };
  if (method1.name === ' construct ' && method2.name === ' construct ') { return true };
  return method1.name === method2.name && method1.static === method2.static;
}

function getNodeMembers(node: ts.Node): Member[] | undefined {
  let members: Member[] | undefined;
  if (ts.isModuleDeclaration(node)) {
    members = (node.body && ts.isModuleBlock(node.body)) ? (node.body.statements as unknown as Member[]) : undefined;
  } else if (ts.isInterfaceDeclaration(node)) {
    members = node.members as unknown as Member[];
  } else if (ts.isClassDeclaration(node)) {
    members = node.members as unknown as Member[];
  } else if (ts.isTypeLiteralNode(node)) {
    members = node.members as unknown as Member[];
  } else if (ts.isSourceFile(node)) {
    members = node.statements as unknown as Member[];
  } else if (ts.isBlock(node)) {
    members = node.statements as unknown as Member[];
  }
  return members;
}